package net.cyweb.cloud.common.api.encrypt.core;

import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.cyweb.cloud.common.api.encrypt.annotation.NoEncrypt;
import net.cyweb.cloud.common.api.encrypt.bean.CryptoInfoBean;
import net.cyweb.cloud.common.api.encrypt.config.ApiEncryptProperties;
import net.cyweb.cloud.common.api.encrypt.util.ApiCryptoUtil;
import net.cyweb.cloud.common.core.constant.SecurityConstants;
import net.cyweb.cloud.common.core.util.WebUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static net.cyweb.cloud.common.api.encrypt.util.ApiCryptoUtil.KEY_EXTRACTORS;

/**
 * 响应数据的加密处理<br>
 * 本类只对控制器参数中含有<strong>{@link org.springframework.web.bind.annotation.ResponseBody}</strong>
 * 或者控制类上含有<strong>{@link org.springframework.web.bind.annotation.RestController}</strong>
 * 以及package为<strong><code>cn.licoy.encryptbody.annotation.encrypt</code></strong>下的注解有效
 *
 * @author licoy.cn
 * @author L.cm
 * @version 2018/9/4
 * @see ResponseBodyAdvice
 */
@Slf4j
@Order(1)
@ControllerAdvice
@RequiredArgsConstructor
@ConditionalOnProperty(value = ApiEncryptProperties.PREFIX + ".enable", havingValue = "true")
public class ApiEncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    private final ApiEncryptProperties properties;

    private final ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter methodParameter, Class converterType) {

        // 非feign调用才进行加密
        if (StrUtil.isNotBlank(WebUtils.getRequest().getHeader(SecurityConstants.FEIGN_USER_AGENT))) {
            return false;
        }

        // 上传文件的请求不加密，因为 element 的组件，需要自己定义HTTP ，不能使用全局 http 加密工具解析
        if (Arrays.stream(Objects.requireNonNull(methodParameter.getMethod()).getParameterTypes())
                .anyMatch(type -> MultipartFile.class.isAssignableFrom(type) || InputStream.class.isAssignableFrom(type))) {
            return false;
        }

        if (AnnotationUtil.hasAnnotation(methodParameter.getMethod(), NoEncrypt.class)) {
            return false;
        }

        return properties.getSkipUrl().stream().noneMatch(WebUtils.getRequest().getRequestURI()::contains);
    }

    @Nullable
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

        String key = KEY_EXTRACTORS.get(properties.getDefaultEncryptType()).apply(properties);
        CryptoInfoBean cryptoInfoBean = new CryptoInfoBean(properties.getDefaultEncryptType(), key);

        byte[] bodyJsonBytes;
        try {
            bodyJsonBytes = objectMapper.writeValueAsBytes(body);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }

        // body 内容 json key, 默认：data {"data":"base64加密字符串"}
        String bodyJsonKey = properties.getBodyJsonKey();
        if (StrUtil.isBlank(bodyJsonKey)) {
            return ApiCryptoUtil.encryptData(bodyJsonBytes, cryptoInfoBean);
        }
        Map<String, Object> data = new HashMap<>(2);
        data.put(bodyJsonKey, ApiCryptoUtil.encryptData(bodyJsonBytes, cryptoInfoBean));
        return data;
    }

}
